<?php
/*
 * Mublog, the social blog.
 * Copyright (C) 2011 Antoine d'Otreppe de Bouvette
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

require_once __DIR__ . '/../aspyct/Aspyct.php';

class Post {
    const SLUG_PATTERN = '/^[a-z0-9-]+$/i';
    
    private $id;
    private $slug;
    private $title;
    private $excerpt;
    private $contents;
    private $release;
    private $author;
    private $public;
    
    public function __toString() {
        return $this->title;
    }
    
    public function validateExcerpt() {
        if (!$this->excerpt) {
            throw new UnexpectedValueException(_("Please provide an excerpt."));
        }
    }
    
    public function validateContents() {
        if (!$this->contents) {
            throw new UnexpectedValueException(_('Please write some text.'));
        }
    }
    
    public function validateTitle() {
        if (!$this->title) {
            throw new UnexpectedValueException(_('Please provide a title.'));
        }
    }
    
    public function validateSlug() {
        if (!$this->slug) {
            throw new UnexpectedValueException(_('Please provide a slug.'));
        }
        
        if (!preg_match(self::SLUG_PATTERN, $this->slug)) {
            throw new UnexpectedValueException(
                _('The slug may contain only letters, digits and -'));
        }
    }
    
    public function validateRelease() {
        if ($this->release === false) {
            throw new UnexpectedValueException(
                _('Invalid date format'));
        }
    }
    
    public function getId() {
        return $this->id;
    }

    public function setId($id) {
        $this->id = $id;
    }

    public function getSlug() {
        return $this->slug;
    }

    public function setSlug($slug) {
        $this->slug = $slug;
    }

    public function getTitle() {
        return $this->title;
    }

    public function setTitle($title) {
        $this->title = $title;
    }

    public function getExcerpt() {
        return $this->excerpt;
    }

    public function setExcerpt($excerpt) {
        $this->excerpt = $excerpt;
    }

    public function getContents() {
        return $this->contents;
    }

    public function setContents($contents) {
        $this->contents = $contents;
    }

    public function getRelease() {
        return $this->release;
    }

    public function setRelease($release) {
        $this->release = $release;
    }

    public function getAuthor() {
        return $this->author;
    }

    public function setAuthor($author) {
        $this->author = $author;
    }

    public function getPublic() {
        return $this->public;
    }

    public function setPublic($public) {
        $this->public = $public;
    }
}

class BlogSettings {
    private $logo;
    private $name;
    private $mission;

    public function getLogo() {
        return $this->logo;
    }

    public function setLogo($logo) {
        $this->logo = $logo;
    }

    public function getName() {
        return $this->name;
    }

    public function setName($name) {
        $this->name = $name;
    }

    public function getMission() {
        return $this->mission;
    }

    public function setMission($mission) {
        $this->mission = $mission;
    }

}

interface BlogSettingsManager {
	/**
	 * Fetch blog settings from default location
	 * @return Blog
	 */
	function getBlogSettings();
	
	/**
	 * Update blog settings
	 * @param BlogSettings $settings
	 */
	function updateBlogSettings(BlogSettings $settings);
}

class FakeBlogSettingsManager implements BlogSettingsManager {
	private $settings;
	
	public function __construct($name, $logo, $mission) {
		$this->settings = new BlogSettings();
		$this->settings->setName($name);
		$this->settings->setLogo($logo);
		$this->settings->setMission($mission);
	}
	
	public function getBlogSettings() {
		return $this->settings;
	}
	
	public function updateBlogSettings(BlogSettings $settings) {
		throw new RuntimeException("Not yet implemented");
	}
}

interface PostDao {
    /**
     * @return Post
     */
    function getPostById($id);
    
    function savePost(Post $post);
    
    function listPosts($offset, $max);
    
    function listPublicPosts($offset, $max);
}

class SimplePostDao extends BaseDao implements PostDao {
    public function __construct() {
        parent::__construct('post');
    }
    
    /**
     *
     * @param int $offset
     * @param int $max
     * @return Fetchable<Post>
     */
    public function listPosts($offset, $max) {
    	return $this->findWithLimit(array(), array(), array('*'),
                                    $offset, $max);	
    }
    
    /**
     *
     * @param int $offset
     * @param int $max
     * @return Fetchable<Post>
     */
    public function listPublicPosts($offset, $max) {
        return $this->findWithLimit(array('public'=>true), array(
            "release <= datetime('now', 'localtime')"
        ), array('*'), $offset, $max);
    }
    
    /**
     * @param int $id
     * @return Post
     */
    public function getPostById($id) {
        return $this->find(array('id' => $id))->fetch();
    }
    
    /**
     * Save a post into the database
     * @param Post $post
     * @return void
     */
    public function savePost(Post $post) {
        if ($post->getId()) {
            $this->updatePost($post);
        }
        else {
            $this->insertPost($post);
        }
    }
    
    /**
     * @param Post $post
     * @return boolean
     */
    private function updatePost(Post $post) {
    	$values = $this->extract($post);
        unset($values['id']);
        
    	$filter = array('id' => $post->getId());
    	return $this->update($values, $filter) === 1;
    }
    
    /**
     * @param Post $post 
     * @return void
     */
    private function insertPost(Post $post) {
    	$values = $this->extract($post);
        unset($values['id']);
        
    	$id = $this->insert($values, true);
    	$post->setId($id);
    }
    
    /**
     * @param array $row
     * @param Post $into
     * @return Post 
     */
    public function convertToPhp(array $row, Post $into = null) {
        if ($into === null) {
            $into = new Post();
        }
        
        if (array_key_exists('release', $row)) {
            $into->setRelease(new DateTime($row['release']));
            unset($row['release']);
        }
        object_inject($into, $row);
        
        return $into;
    }
    
    /**
     * @param Post $post
     * @return array
     */
    private function extract(Post $post) {
        $values = object_extract($post, array(
            'title',
            'slug',
            'contents',
            'release',
            'public',
            'author',
            'excerpt',
            'id'
        ));
        
        if ($values['release'] !== null) {
            // Convert DateTime to SQL DateTime
            $values['release'] = $values['release']->format('Y-m-d H:i:s');
        }
        
        return $values;
    }
}

class ArrayTransformer {
    private $vars;
    
    public function __construct(array $vars) {
        $this->vars = $vars;
    }
    
    public function __call($method, $args) {
        if (substr($method, 0, 3) === 'get') {
            $attr = lcfirst(substr($method, 3));
            
            if (array_key_exists($attr, $this->vars)) {
                return $this->vars[$attr];
            }
            else {
                throw new RuntimeException("No such key in array: $attr");
            }
        }
    }
}

class MublogController extends Controller {
    private $postDao;
    private $authenticator;
    private $blogSettingsManager;
    
    protected function render($template, Request $request,
    	$vars=array(), $inheritable=array()) {
    	
    	return parent::render($template, $request, $vars, array_merge(array(
    		'blog' => $this->blogSettingsManager->getBlogSettings(),
    		'user' => $this->authenticator->getCurrentUser($request)
    	), $inheritable));
    }
    
    /**
     * @param Request $request
     * @return MublogVisitor
     */
    protected function getCurrentUser(Request $request) {
        return $this->authenticator->getCurrentUser($request);
    }
    
    protected function requireAccess($request, $level) {
        if (!$this->getCurrentUser($request)->may($level)) {
            $this->redirect('login');
        }
    }
    
    public function getPostDao() {
        return $this->postDao;
    }

    public function setPostDao($postDao) {
        $this->postDao = $postDao;
    }

    public function getAuthenticator() {
        return $this->authenticator;
    }

    public function setAuthenticator($authenticator) {
        $this->authenticator = $authenticator;
    }

    public function getBlogSettingsManager() {
        return $this->blogSettingsManager;
    }

    public function setBlogSettingsManager($blogSettingsManager) {
        $this->blogSettingsManager = $blogSettingsManager;
    }
}

class MublogHomeController extends MublogController {
    public function do_GET(Request $request) {
        $session = $request->getSessionContainer($this);
        
    	$posts = $this->getPostDao()->listPublicPosts(10, 0);
    	
        return $this->render('home', $request, array(
            'posts' => $posts
        ));
    }
}

class MublogPostController extends MublogController {
    public function do_GET(Request $request) {
    	$post = $this->getPostDao()->getPostById($request->getArgument('id'));
        $vars = array(
            'post' => $post
        );
        
        return $this->render('post', $request, $vars);
    }
}

class MublogAdminController extends MublogController {
    public function preExec(Request $request) {
        $this->requireAccess($request, MublogAccess::ADMIN);
    }
    
    public function do_GET(Request $request) {
        return $this->render('admin', $request);
    }
}

class MublogNewPostController extends MublogController {
    private $form;
    private $post;
    
    public function preExec(Request $request) {
        $this->requireAccess($request, MublogAccess::WRITE);
    }
    
    private function getForm() {
        if ($this->form === null) {
            $this->form = new MublogPostForm('post');
        }
        
        return $this->form;
    }
    
    public function do_GET(Request $request) {
        if ($this->post === null) {
            $this->post = new Post();
            $this->post->setPublic(true);
        }
        
        $vars = array(
            'post' => $this->post,
            'action' => 'new',
            'form' => $this->getForm()
        );
        
        return $this->render('edit-post', $request, $vars);
    }
    
    public function do_POST(Request $request) {
        $post = new Post();
        
        $form = $this->getForm();
        $form->parseInto($post, $request);
        
        if ($form->validate($post)) {
            $author = $this->getCurrentUser($request)->getUser();
            $post->setAuthor($author->getName());
            $this->getPostDao()->savePost($post);
            return $this->redirect('post.view', $post);
        }
        else {
            $this->post = $post;
            return $this->do_GET($request);
        }
    }
}

class MublogEditPostController extends MublogController {
    private $form;
    private $post;
    
    public function preExec(Request $request) {
        $this->requireAccess($request, MublogAccess::WRITE);
    }
    
    public function do_GET(Request $request) {
        $post = $this->getPostDao()->getPostById($request->getArgument('id'));
        $vars = array(
            'post' => $post,
            'action' => 'edit',
            'form' => $this->getForm()
        );
        
        return $this->render('edit-post', $request, $vars);
    }
    
    public function do_POST(Request $request) {
        $postDao = $this->getPostDao();
        $post = $postDao->getPostById($request->getArgument('id', null));
        
        $form = $this->getForm();
        $form->parseInto($post, $request);
        
        if ($form->validate($post)) {
            $this->getPostDao()->savePost($post);
            return $this->redirect('post.view', $post);
        }
        else {
            $this->post = $post;
            return $this->do_GET($request);
        }
    }
    
    private function getForm() {
        if ($this->form === null) {
            $this->form = new MublogPostForm('post');
        }
        
        return $this->form;
    }
}

class MublogLogoutController extends MublogController {
    public function do_GET(Request $request) {
        $this->getAuthenticator()->logout($request);
        $this->redirect('home');
    }
}

class MublogLoginController extends MublogController {
    private $form;
    private $login;
    
    public function do_GET(Request $request) {
        if ($this->login === null) {
            $this->login = new Login();
        }
        
        return $this->render('login', $request, array(
            'form' => $this->getForm(),
            'login' => $this->login
        ));
    }
    
    public function do_POST(Request $request) {
        $this->login = new Login();
        
        $form = $this->getForm();
        $form->parseInto($this->login, $request);
        
        if ($form->validate($this->login)) {
            if ($this->getAuthenticator()->login($request,
                    $this->login->getUsername(), $this->login->getPassword())) {
                return $this->redirect('admin.dashboard');
            }
        }
        
        return $this->do_GET($request);
    }
    
    private function getForm() {
        if ($this->form === null) {
            $this->form = new MublogLoginForm('login');
        }
        
        return $this->form;
    }
}

class MublogLoginForm extends AbstractForm {
    public function init() {
        $this
            ->add('username', _('Username'))
            ->add('password', _('Password'), null,
                    new InputWidget(InputWidget::TYPE_PASSWORD));
    }
    
    public function validate(Login $login) {
        return $this->validateProperties($login, 'username', 'password');
    }
}

class MublogPostForm extends AbstractForm {
    public function init() {
        $this
            ->add('title', _('Title'))
            ->add('slug', _('Slug'), _('The slug will be used in the post URL'))
            ->add('excerpt', _('Excerpt'), _('A quick summary of the post'),
                    new TextareaWidget())
            ->add('contents', _('Contents'), null, new TextareaWidget())
            ->add('release', _('Release date'), null,
                    new InputWidget(InputWidget::TYPE_TEXT,
                            // FIXME Use local date format
                            new DateTimeFormatter('Y/m/d H:i')))
            ->add('public', null, null, new CheckboxWidget(_('Public')));
    }
    
    public function validate(Post $post) {
        return $this->validateProperties($post,
            'title', 'slug', 'excerpt', 'contents', 'release', 'public');
    }
}

class Login {
    private $username;
    private $password;
    
    public function validateUsername() {
        $this->username = $this->username;
        
        if (!$this->username) {
            throw new UnexpectedValueException(
                    _('Plase enter your username or e-mail address.'));
        }
    }
    
    public function validatePassword() {
        if (!$this->password) {
            throw new UnexpectedValueException(_('Pleas enter your password.'));
        }
    }
    
    public function getUsername() {
        return $this->username;
    }

    public function setUsername($username) {
        $this->username = $username;
    }

    public function getPassword() {
        return $this->password;
    }

    public function setPassword($password) {
        $this->password = $password;
    }
}

interface MublogAuthenticator {
    function getCurrentUser(Request $request);
    function login(Request $request, $username, $password);
    function createUser(User $user);
    function logout(Request $request);
}

class FakeMublogAuthenticator implements MublogAuthenticator {
    public function getCurrentUser(Request $request) {
        $user = new User();
        $user->setName('Antoine_935');
        $user->setEmail('a.dotreppe@aspyct.org');
        
        return new MublogAuthenticatedUser($user);
    }
    
    public function login(Request $request, $username, $password) {
        if ($username === 'Antoine_935' && $password === 'hello') {
            return true;
        }
    }
    
    public function logout(Request $request) {
        
    }
    
    public function createUser(User $user) {
        throw new RuntimeException(
            "Cannot create a user with the FakeMublogAuthenticator");
    }
}

class SimpleMublogAuthenticator implements MublogAuthenticator {
    private $userDao;
    private $currentUser;
    
    public function getCurrentUser(Request $request) {
        $container = $request->getSessionContainer($this);
        if (isset($container->userId)) {
            $user = $this->userDao->findById($container->userId);
            
            if ($user != null) {
                return new MublogAuthenticatedUser($user);
            }
        }
        
        return new MublogVisitor();
    }
    
    public function getUserDao() {
        return $this->userDao;
    }

    public function setUserDao($userDao) {
        $this->userDao = $userDao;
    }
    
    public function login(Request $request, $username, $password) {
        $user = $this->userDao->findByName($username);
        
        if ($user !== null) {
            $providedPass = $this->crypt($username, $password,
                    $user->getSalt());
            
            if ($providedPass === $user->getPassword()) {
                // Success
                $container = $request->getSessionContainer($this);
                $container->userId = $user->getId();
                return true;
            }
        }
        
        return false;
    }
    
    public function logout(Request $request) {
        $container = $request->getSessionContainer($this);
        
        if (isset($container->userId)) {
            unset($container->userId);
        }
    }
    
    public function createUser(User $user) {
        $salt = uniqid(null, true);
        $user->setSalt($salt);
        $user->setPassword(
                $this->crypt($user->getName(), $user->getPassword(), $salt));
        
        $this->userDao->saveUser($user);
        return $user;
    }
    
    private function crypt($user, $password, $salt) {
        return sha1(sha1($user . $password) . $salt);
    }
}

class SimpleUserDao extends BaseDao {
    public function __construct() {
        parent::__construct('User', 'user');
    }
    
    public function findByName($username) {
        return $this->find(array('name' => $username))->fetch();
    }
    
    public function findById($id) {
        return $this->find(array('id' => $id))->fetch();
    }
    
    public function saveUser(User $user) {
        if ($user->getId()) {
            $this->updateUser($user);
        }
        else {
            $this->insertUser($user);
        }
    }
    
    private function updateUser(User $user) {
        $values = $user->extract('name', 'password', 'salt', 'email');
        $filter = $user->extract('id');
        return $this->update($values, $filter);
    }
    
    private function insertUser(User $user) {
        $values = $user->extract('name', 'password', 'salt', 'email');
        $id = $this->insert($values, true);
        $user->setId($id);
    }
    
    /**
     * @param array $row
     * @param User $into 
     */
    public function convertToPhp(array $row, User $into = null) {
        if ($into === null) {
            $into = new User();
        }
        
        object_inject($into, $row);
        return $into;
    }
}

class User {
    private $id;
    private $name;
    private $password;
    private $salt;
    private $email;
    
    public function getId() {
        return $this->id;
    }

    public function setId($id) {
        $this->id = $id;
    }

    public function getName() {
        return $this->name;
    }

    public function setName($name) {
        $this->name = $name;
    }

    public function getPassword() {
        return $this->password;
    }

    public function setPassword($password) {
        $this->password = $password;
    }

    public function getSalt() {
        return $this->salt;
    }

    public function setSalt($salt) {
        $this->salt = $salt;
    }

    public function getEmail() {
        return $this->email;
    }

    public function setEmail($email) {
        $this->email = $email;
    }
}

class MublogUserForm extends AbstractForm {
    public function __construct($namespace=null) {
        parent::__construct($namespace ? $namespace : 'user');
    }
    
    protected function init() {
        $this
            ->add('name', _('Name'))
            ->add('password', _('Password'), null,
                    new InputWidget(InputWidget::TYPE_PASSWORD))
            ->add('email', _('e-Mail address'));
    }
    
    public function validate(User $user) {
        return $this->validateProperties($user, 'name', 'password', 'email');
    }
}

interface MublogAccess {
    const READ = 1;
    const ADMIN = 2;
    const WRITE = 2;
}

class MublogVisitor {
    public function may($level) {
        return $level <= MublogAccess::READ;
    }
    
    public function isLoggedIn() {
        return false;
    }
}

class MublogAuthenticatedUser extends MublogVisitor {
    private $entity;
    
    public function __construct(User $user) {
        $this->entity = $user;
    }
    
    public function may($level) {
        return $level <= MublogAccess::ADMIN;
    }
    
    public function isLoggedIn() {
        return true;
    }
    
    public function getUser() {
        return $this->entity;
    }
}



abstract class BaseMublogEnvironment extends Environment {
    public function getPostDao() {
    	$dao = new SimplePostDao();
    	$this->initDao($dao);
        
    	return $dao;
    }
    
    public function getUserDao() {
        $dao = new SimpleUserDao();
        $this->initDao($dao);
        
        return $dao;
    }
    
    public function getRoutes() {
        return array(
            '/' => 'home',
            '/home/{offset}' => 'home.offset',
            '/{id:\d+}-{slug}' => 'post.view',
            '/edit/{id:\d+}' => 'post.edit',
            '/admin/list' => 'post.list',
            '/write' => 'post.new',
            '/admin' => 'admin.dashboard',
            '/login' => 'login',
            '/logout' => 'logout'
        );
    }
    
    public function getRouteMapping() {
        return array(
            'home' => 'homeController',
            'home.offset' => 'homeController',
            'post.view' => 'postController',
            'post.edit' => 'editPostController',
            'post.new' => 'newPostController',
            'admin.dashboard' => 'adminController',
            'login' => 'loginController',
            'logout' => 'logoutController'
        );
    }
    
    public function getConfig() {
        $domain = isset($_SERVER['HTTP_HOST']) ?
            $_SERVER['HTTP_HOST'] : 'default';
        
        return array_merge(parent::getConfig(), array(
            'database.dsn' => 'sqlite:' . __DIR__.'/../data/'.$domain,
            'template.source' => __DIR__.'/../resources/templates',
            'log.file' => realpath(__DIR__.'/..//data/log')
        ));
    }
    
    public function getHomeController() {
        $controller = new MublogHomeController();
        $this->initMublogController($controller);
        
        return $controller;
    }
    
    public function getPostController() {
        $controller = new MublogPostController();
        $this->initMublogController($controller);
        
        return $controller;
    }
    
    public function getAdminController() {
        $controller = new MublogAdminController();
        $this->initMublogController($controller);
        
        return $controller;
    }
    
    public function getEditPostController() {
        $controller = new MublogEditPostController();
        $this->initMublogController($controller);
        
        return $controller;
    }
    
    public function getNewPostController() {
        $controller = new MublogNewPostController();
        $this->initMublogController($controller);
        
        return $controller;
    }
    
    public function getLoginController() {
        $controller = new MublogLoginController();
        $this->initMublogController($controller);
        
        return $controller;
    }
    
    
    public function getAuthenticator() {
        $authenticator = new SimpleMublogAuthenticator();
        $authenticator->setUserDao($this->userDao);
        
        return $authenticator;
    }
    
    public function getLogoutController() {
        $controller = new MublogLogoutController();
        $this->initMublogController($controller);
        
        return $controller;
    }
    
    protected function initMublogController(MublogController $controller) {
        parent::initController($controller);
        $controller->setPostDao($this->postDao);
        $controller->setAuthenticator($this->authenticator);
        $controller->setBlogSettingsManager($this->blogSettingsManager);
    }
    
    public function getBlogSettingsManager() {
        $mngr = new SimpleBlogSettingsManager();
        $mngr->setDatabase($this->database);
        
        return $mngr;
    }
}

class SimpleBlogSettingsManager implements BlogSettingsManager {
    const TABLE = 'settings';
    
    private static $KEYS = array(
        'mublog_logo' => 'logo',
        'mublog_name' => 'name',
        'mublog_mission' => 'mission'
    );
    
    /**
     * @var Database
     */
    private $database;
    
    public function getBlogSettings() {
        $query = "SELECT key, value FROM #" . self::TABLE . " WHERE key IN (?)";
        $ps = $this->database->query($query, array_keys(self::$KEYS));
        
        $settings = new BlogSettings();
        while (($row = $ps->fetch()) !== false) {
            $property = self::$KEYS[$row[0]];
            $settings->{'set'.ucfirst($property)}($row[1]);
        }
        
        return $settings;
    }
    
    public function updateBlogSettings(BlogSettings $settings) {
        $update = "UPDATE #".self::TABLE." SET value = ? WHERE key = ?";
        $insert = "INSERT INTO #".self::TABLE." (key, value) VALUES (?, ?)";
        
        foreach (self::$KEYS as $key=>$property) {
            $value = $settings->{'get'.ucfirst($property)}();
            
            $ps = $this->database->query($update, $value, $key);
            if ($ps->rowCount() === 0) {
                // Nothing has been updated, need to be inserted
                $this->database->query($insert, $key, $value);
            }
        }
    }
    
    public function setDatabase(Database $database) {
        $this->database = $database;
    }
    
    public function getDatabase() {
        return $this->database;
    }
}
